package com.example.framework.redis.core.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.example.framework.common.util.json.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @author yuzhenxin
 * @date 2023/8/1
 */
@Slf4j
@Component
public class RedisUtils {

    private static RedissonClient redissonClient;

    private static StringRedisTemplate stringRedisTemplate;

    @Resource
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        RedisUtils.stringRedisTemplate = stringRedisTemplate;
    }

    @Autowired
    public void setRedisTemplate(RedissonClient redissonClient) {
        RedisUtils.redissonClient = redissonClient;
    }

    private static final Long GET_TIME_OUT = 2L;

    private static final Long LOCK_TIME_OUT = 30L;

    private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;

    // 获取锁默认的睡眠时间，单位毫秒
    private static final long SLEEP_INTERVAL_MILLIS = 100L;

    /* ---------------- String ---------------- */

    public static void set(String key, Object value) {
        set(key, value, -1L, DEFAULT_TIME_UNIT);
    }

    public static void set(String key, Object value, long time, TimeUnit timeUnit) {
        if (time > 0L) {
            stringRedisTemplate.opsForValue().set(key, JsonUtils.toJsonString(value), time, timeUnit);
        } else {
            stringRedisTemplate.opsForValue().set(key, JsonUtils.toJsonString(value));
        }
    }

    public static boolean hasKey(String key) {
        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
    }

    public static String getString(String key) {
        return get(key, String.class);
    }

    public static <T> T get(String key, Class<T> clazz) {
        String value = stringRedisTemplate.opsForValue().get(key);
        return stringToObject(value, clazz);
    }

    public static <T> List<T> getArray(String key, Class<T> clazz) {
        String value = stringRedisTemplate.opsForValue().get(key);
        return JsonUtils.parseArray(value, clazz);
    }

    public static void increment(String key) {
        stringRedisTemplate.opsForValue().increment(key);
    }

    public static void increment(String key, long delta) {
        stringRedisTemplate.opsForValue().increment(key, delta);
    }

    public static void decrement(String key, long delta) {
        stringRedisTemplate.opsForValue().decrement(key, delta);
    }

    /* ---------------- Hash ---------------- */

    public static void hSet(String key, String item, Object value) {
        hSet(key, item, value, -1, DEFAULT_TIME_UNIT);
    }

    public static void hSet(String key, String item, Object value, long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForHash().put(key, item, JsonUtils.toJsonString(value));
        if (time > 0L) {
            expire(key, time, timeUnit);
        }
    }

    public static boolean hmSet(String key, Map<String, Object> map) {
        return hmSet(key, map, -1, DEFAULT_TIME_UNIT);
    }

    public static Boolean hmSet(String key, Map<String, Object> map, long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForHash().putAll(key, map);
        if (time > 0L) {
            return expire(key, time, timeUnit);
        }
        return true;
    }

    public static boolean hHasKey(String key, String item) {
        return stringRedisTemplate.opsForHash().hasKey(key, item);
    }

    public static <T> T hGet(String key, String item, Class<T> clazz) {
        String value = (String) stringRedisTemplate.opsForHash().get(key, item);
        return stringToObject(value, clazz);
    }

    public static Map<Object, Object> hmGet(String key) {
        return stringRedisTemplate.opsForHash().entries(key);
    }

    public static double hIncr(String key, String item, double by) {
        return stringRedisTemplate.opsForHash().increment(key, item, by);
    }

    public static double hDecr(String key, String item, double by) {
        return stringRedisTemplate.opsForHash().increment(key, item, -by);
    }

    public static void hDel(String key, Object... item) {
        stringRedisTemplate.opsForHash().delete(key, item);
    }

    /* ---------------- List ----------------*/

    public static void lPush(String key, Object value) {
        lPush(key, value, -1, DEFAULT_TIME_UNIT);
    }

    public static void lPush(String key, Object value, long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForList().leftPush(key, JsonUtils.toJsonString(value));
        expire(key, time, timeUnit);
    }

    public static void rPush(String key, Object value) {
        rPush(key, value, -1, DEFAULT_TIME_UNIT);
    }

    public static void rPush(String key, Object value, long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForList().rightPush(key, JsonUtils.toJsonString(value));
        expire(key, time, timeUnit);
    }

    public static <T> T lPop(String key, Class<T> clazz) {
        String value = stringRedisTemplate.opsForList().leftPop(key);
        return stringToObject(value, clazz);
    }

    public static <T> T rPop(String key, Class<T> clazz) {
        String value = stringRedisTemplate.opsForList().rightPop(key);
        return stringToObject(value, clazz);
    }

    public static List<String> lRange(String key, long start, long end) {
        return stringRedisTemplate.opsForList().range(key, start, end);
    }

    public static Long lSize(String key) {
        return stringRedisTemplate.opsForList().size(key);
    }

    public static Boolean lSet(String key, long index, Object value) {
        stringRedisTemplate.opsForList().set(key, index, JsonUtils.toJsonString(value));
        return true;
    }

    /**
     * @param key   键
     * @param count count > 0 : 从表头开始向表尾搜索，移除与 VALUE 相等的元素，数量为 COUNT 。
     *              count < 0 : 从表尾开始向表头搜索，移除与 VALUE 相等的元素，数量为 COUNT 的绝对值。
     *              count = 0 : 移除表中所有与 VALUE 相等的值。
     * @param value 要移除的参数
     * @return 被移除元素的数量
     */
    public static Long lRemove(String key, long count, Object value) {
        return stringRedisTemplate.opsForList().remove(key, count, value);
    }

    /** --------------------set相关操作-------------------------- */

    /**
     * set添加元素
     *
     * @param key
     * @param values
     * @return
     */
    public static Long sAdd(String key, String... values) {
        return stringRedisTemplate.opsForSet().add(key, values);
    }

    public static Long sAdd(String key, long timeout, TimeUnit unit, String... values) {
        stringRedisTemplate.expire(key, timeout, unit);
        return stringRedisTemplate.opsForSet().add(key, values);
    }

    /**
     * set移除元素
     *
     * @param key
     * @param values
     * @return
     */
    public static Long sRemove(String key, Object... values) {
        return stringRedisTemplate.opsForSet().remove(key, values);
    }

    /**
     * 移除并返回集合的一个随机元素
     *
     * @param key
     * @return
     */
    public static String sPop(String key) {
        return stringRedisTemplate.opsForSet().pop(key);
    }

    /**
     * 将元素value从一个集合移到另一个集合
     *
     * @param key
     * @param value
     * @param destKey
     * @return
     */
    public static Boolean sMove(String key, String value, String destKey) {
        return stringRedisTemplate.opsForSet().move(key, value, destKey);
    }

    /**
     * 获取集合的大小
     *
     * @param key
     * @return
     */
    public static Long sSize(String key) {
        return stringRedisTemplate.opsForSet().size(key);
    }

    /**
     * 判断集合是否包含value
     *
     * @param key
     * @param value
     * @return
     */
    public static Boolean sIsMember(String key, Object value) {
        return stringRedisTemplate.opsForSet().isMember(key, value);
    }

    /**
     * 获取两个集合的交集
     *
     * @param key
     * @param otherKey
     * @return
     */
    public static Set<String> sIntersect(String key, String otherKey) {
        return stringRedisTemplate.opsForSet().intersect(key, otherKey);
    }

    /* ---------------- ZSet ----------------*/

    public static void zAdd(String key, Object value, double score) {
        stringRedisTemplate.opsForZSet().add(key, JsonUtils.toJsonString(value), score);
    }

    public static Set<String> zRange(String key, long start, long end) {
        return stringRedisTemplate.opsForZSet().range(key, start, end);
    }

    public static Set<String> zRangeByScore(String key, double min, double max) {
        return stringRedisTemplate.opsForZSet().rangeByScore(key, min, max);
    }

    public static Long zRemoveByScore(String key, double min, double max) {
        return stringRedisTemplate.opsForZSet().removeRangeByScore(key, min, max);
    }

    public static Long zRemove(String key, Object... values) {
        return stringRedisTemplate.opsForZSet().remove(key, values);
    }

    public static ZSetOperations.TypedTuple<String> popMax(String key) {
        return stringRedisTemplate.opsForZSet().popMax(key);
    }

    public static ZSetOperations.TypedTuple<String> popMin(String key) {
        return stringRedisTemplate.opsForZSet().popMin(key);
    }

    /* ---------------- common ----------------*/

    public static Boolean expire(String key, long time, TimeUnit timeUnit) {
        if (time < 0L) {
            return false;
        }
        return stringRedisTemplate.expire(key, time, timeUnit);
    }

    public static Boolean delete(String key) {
        if (key != null) {
            return stringRedisTemplate.delete(key);
        }
        return Boolean.FALSE;
    }

    public static Long delete(String... key) {
        // 此处Arrays.asList生成的为不可变的list
        return stringRedisTemplate.delete(new ArrayList<>(Arrays.asList(key)));
    }

    /* ---------------- lock ----------------*/

    public static Boolean tryLock(String key, String value, long lockTimeOut, TimeUnit timeUnit) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, value, lockTimeOut, timeUnit);
    }

    public static Boolean lock(String key) {
        return lock(key, key, LOCK_TIME_OUT, GET_TIME_OUT, DEFAULT_TIME_UNIT);
    }

    public static Boolean lock(String key, long getTimeOut, TimeUnit timeUnit) {
        return lock(key, key, LOCK_TIME_OUT, getTimeOut, timeUnit);
    }

    public static Boolean lock(String key, String value) {
        return lock(key, value, LOCK_TIME_OUT, GET_TIME_OUT, DEFAULT_TIME_UNIT);
    }

    public static Boolean lock(String key, String value, long getTimeOut, TimeUnit timeUnit) {
        return lock(key, value, LOCK_TIME_OUT, getTimeOut, timeUnit);
    }

    /**
     * 尝试获取锁，如果获取失败，则一直尝试获取，直到获取成功或者超时
     *
     * @param key         锁的key
     * @param value       锁的值
     * @param lockTimeOut 锁的过期时间
     * @param getTimeOut  获取锁的超时时间（单位秒）
     * @param timeUnit    锁的过期时间和获取锁的超时时间单位
     */

    public static Boolean lock(String key, String value, long lockTimeOut, long getTimeOut, TimeUnit timeUnit) {
        long startTime = System.nanoTime();
        while (true) {
            if (tryLock(key, value, lockTimeOut, timeUnit)) {
                return true;
            } else {
                // 使用纳秒级的睡眠来减少CPU占用，同时避免使用while(true) + Thread.sleep造成的精度问题
                LockSupport.parkNanos(TimeUnit.NANOSECONDS.convert(SLEEP_INTERVAL_MILLIS, TimeUnit.MICROSECONDS));
                if (Thread.interrupted()) {
                    // 记录日志以便于问题追踪
                    log.error("Thread interrupted while attempting to lock with key: {}, value: {}", key, value);
                    Thread.currentThread().interrupt();
                    return false;
                }

                if (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) > TimeUnit.MILLISECONDS.convert(getTimeOut, timeUnit)) {
                    return false;
                }
            }
        }
    }

    /**
     * 定时任务执行器（有redis分布式锁控制，保证不重复执行）
     *
     * @param taskName   任务名称
     * @param lockKey    分布式锁KEY
     * @param lockTime   加锁时间
     * @param timeUnit   时间单位
     * @param taskRunner 任务执行体
     * @return 是否执行成功
     */
    public static boolean tryRunTask(String taskName, String lockKey, long lockTime, TimeUnit timeUnit, Runnable taskRunner) {
        boolean lock = false;
        // value为线程id
        String threadId = String.valueOf(Thread.currentThread().threadId());
        try {
            lock = tryLock(lockKey, threadId, lockTime, timeUnit);
            if (lock) {
                log.info("quartz 【{}】 获取锁成功: lockKey【{}】", taskName, lockKey);
                taskRunner.run();
                return true;
            } else {
                log.info("quartz 【{}】 获取锁失败: lockKey【{}】", taskName, lockKey);
            }
        } catch (Exception e) {
            log.error("quartz 【{}】 定时任务异常: {}", taskName, e.getMessage());
        } finally {
            // 加锁成功？线程是否还持有锁（锁过期则不持有）？
            if (lock) {
//                boolean unLock = Objects.equals(getString(lockKey), threadId) && unLock(lockKey); // 此处判断和释放锁非原子操作，如果宕机会无法释放锁，使用lua脚本解决
                boolean unLock = unLock(lockKey, threadId);
                if (unLock) {
                    log.info("释放锁成功: lockKey【{}】", lockKey);
                } else {
                    log.info("释放锁失败: lockKey【{}】", lockKey);
                }
            }
        }
        return false;
    }

    public static boolean unLock(String key) {
        return Boolean.TRUE.equals(stringRedisTemplate.delete(key));
    }

    private static boolean unLock(String lockKey, String value) {
        // Lua 脚本解锁
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Long execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), value);
        return Objects.equals(execute, 1L);
    }

    /**
     * 定时任务执行器（有redis分布式锁控制，保证不重复执行）
     *
     * @param taskName   任务名称
     * @param lockKey    分布式锁KEY
     * @param lockTime   加锁时间
     * @param timeUnit   时间单位
     * @param taskRunner 任务执行体
     */
    public static void runTask(String taskName, String lockKey, long lockTime, TimeUnit timeUnit, Runnable taskRunner) {
        RLock redissonLock = redissonClient.getLock(lockKey);
        try {
            // 阻塞加锁
            redissonLock.lock(lockTime, timeUnit);
            taskRunner.run();
        } catch (Exception e) {
            log.error("quartz 【{}】 定时任务异常: ", taskName, e);
        } finally {
            // 释放锁
            redissonLock.unlock();
        }
    }

    private static <T> T stringToObject(String value, Class<T> clazz) {
        return JsonUtils.parseObject(value, clazz);
    }

    /**
     * 返回 key 的剩余的过期时间
     */
    public static Long getExpire(String key, TimeUnit unit) {
        return stringRedisTemplate.getExpire(key, unit);
    }

    /**
     * 返回 key 的剩余的过期时间
     */
    public static Long getExpire(String key) {
        return stringRedisTemplate.getExpire(key);
    }

    public static Set<String> scan(String pattern) {
        return scan(pattern, 1000);
    }

    public static Set<String> scan(String pattern, int count) {
        if (StrUtil.isBlank(pattern)) {
            return CollUtil.newHashSet();
        }
        return stringRedisTemplate.execute((RedisCallback<Set<String>>) connection -> {
            Set<String> keys = new HashSet<>();
            ScanOptions options = ScanOptions.scanOptions()
                    .match(pattern)
                    .count(count)
                    .build();
            try (Cursor<byte[]> cursor = connection.keyCommands().scan(options)) {
                while (cursor.hasNext()) {
                    keys.add(new String(cursor.next(), StandardCharsets.UTF_8));
                }
            } catch (Exception e) {
                log.error("scan pattern {} is error", pattern, e);
            }
            return keys;
        });
    }

}
